package controleur;

import java.util.ArrayList;
import java.util.List;

import exception.HypothesesNonRespecteesException;

import modele.GrapheCase;
import modele.Ressources;
import modele.Statut;

public class ControleurGrapheCase {
	// Variable(s) d'instance
	private final GrapheCase graphe;
	
	// Constructeur(s)
	public ControleurGrapheCase() {
		this.graphe = new GrapheCase();
		this.initialiser();
	}
	
	public ControleurGrapheCase(ControleurGrapheCase grapheCase) {
		this.graphe=new GrapheCase();
		for(int i = 0; i < this.taille(); i++) {
			for(int j = 0; j < this.taille(); j++) {
				this.set(i, j, new ControleurCase(grapheCase.get(i, j)));
			}
		}
	}
	
	// Methode(s)
	public void initialiser() {
		// Initialise le graphe en mettant toutes les cases accessibles.
		// Rend les voisins accessibles entre eux.
		// Le reste est initialise a faux
		boolean trouve = false;
		int indiceVoisins;
		ControleurCase casePlateau;
		List<ControleurCoordonnee> voisins;
		
		for(int i = 0; i < this.taille(); i++) {
			voisins=ControleurGrapheCase.voisins(i);
			for(int j = 0; j < this.taille(); j++) {
				indiceVoisins=0;
				casePlateau = new ControleurCase();
				if(i != j) {
					trouve = false;
					casePlateau.setStatut(Statut.INACCESSIBLE);
					while(indiceVoisins < voisins.size() && !trouve) {
						if(ControleurGrapheCase.getCoordonnee(j).equals(voisins.get(indiceVoisins))) {
							trouve = true;
							casePlateau.setStatut(Statut.ACCESSIBLE);
						}
						indiceVoisins++;
					}
				}
				this.graphe.getMatriceDAdjacence()[i][j] = casePlateau;
			}
		}
	}
	
	public int taille() {
		return this.graphe.getMatriceDAdjacence().length;
	}
	
	public ControleurCase get(int i, int j) {
		return this.graphe.getMatriceDAdjacence()[i][j];
	}
	
	public ControleurCase get(ControleurCoordonnee coordonnee) {
		// Les coordonnees passees en parametre sont des coordonnees sur le plateau
		// Il faut donc au prealable les convertir en numero de case pour pouvoir
		// les utiliser dans le graphe
		int numeroDeCase = ControleurGrapheCase.getNumeroDeCase(coordonnee.getX(), coordonnee.getY());
		
		return this.graphe.getMatriceDAdjacence()[numeroDeCase][numeroDeCase];
	}
	
	public ControleurCase get(ControleurCoordonnee coordonnee1, ControleurCoordonnee coordonnee2) {
		// Les coordonnees passees en parametre sont des coordonnees sur le plateau
		// Il faut donc au prealable les convertir en numero de case pour pouvoir
		// les utiliser dans le graphe
		int numeroDeCase1 = ControleurGrapheCase.getNumeroDeCase(coordonnee1.getX(), coordonnee1.getY());;
		int numeroDeCase2 = ControleurGrapheCase.getNumeroDeCase(coordonnee2.getX(), coordonnee2.getY());
		
		return this.graphe.getMatriceDAdjacence()[numeroDeCase1][numeroDeCase2];
	}
	
	public void set(ControleurCoordonnee coordonnee, ControleurCase casePlateau) {
		// Les coordonnees passees en parametre sont des coordonnees sur le plateau
		// Il faut donc au prealable les convertir en numero de case pour pouvoir
		// les utiliser dans le graphe
		int numeroDeCase = ControleurGrapheCase.getNumeroDeCase(coordonnee.getX(), coordonnee.getY());
		
		this.graphe.getMatriceDAdjacence()[numeroDeCase][numeroDeCase] = casePlateau;
	}
	
	public void set(int numeroDeCase1, int numeroDeCase2, ControleurCase casePlateau) {
		this.graphe.getMatriceDAdjacence()[numeroDeCase1][numeroDeCase2] = casePlateau;
	}
	
	public void set(ControleurCoordonnee coordonnee1, ControleurCoordonnee coordonnee2, ControleurCase casePlateau) {
		// Les coordonnees passees en parametre sont des coordonnees sur le plateau
		// Il faut donc au prealable les convertir en numero de case pour pouvoir
		// les utiliser dans le graphe
		int numeroDeCase1 = ControleurGrapheCase.getNumeroDeCase(coordonnee1.getX(), coordonnee1.getY());;
		int numeroDeCase2 = ControleurGrapheCase.getNumeroDeCase(coordonnee2.getX(), coordonnee2.getY());
		
		this.graphe.getMatriceDAdjacence()[numeroDeCase1][numeroDeCase2] = casePlateau;
	}
	
	public void changerStatutCasePlateau(int lig, int col, Statut statut, ControleurUnite unite) {
		// Change le statut d'une case d'un PLATEAU
		// c'est a dire d'une case de la diagonale de la MATRICE d'adjacence
		// Cette methode sert a rendre une case libre ou occupee (element de decord ou unite)
		int indiceVoisins = 0;
		int indiceGraphe = 0;
		ControleurCoordonnee coordonneeActuelle = new ControleurCoordonnee(lig, col);
		int numeroCaseActuelle = ControleurGrapheCase.getNumeroDeCase(lig, col);
		String label;
		for(indiceGraphe = 0; indiceGraphe < this.taille(); indiceGraphe++) {
			if(indiceGraphe != numeroCaseActuelle) {
				this.get(indiceGraphe, numeroCaseActuelle).setStatut(Statut.INACCESSIBLE);
			}
			if(unite == null && indiceGraphe != numeroCaseActuelle) {
				this.get(numeroCaseActuelle, indiceGraphe).setStatut(Statut.INACCESSIBLE);
			}
		}
		if(statut.equals(Statut.ACCESSIBLE)) {
			// Si la case doit devenir libre, celle ci ne pourra etre accessible que par ses cases voisines
			label = "O";
			List<ControleurCoordonnee> voisins = this.voisinsAccessibles(numeroCaseActuelle);
			for(indiceVoisins = 0; indiceVoisins < voisins.size(); indiceVoisins++) {
				this.get(coordonneeActuelle, voisins.get(indiceVoisins)).setStatut(Statut.ACCESSIBLE);
				this.get(voisins.get(indiceVoisins), coordonneeActuelle).setStatut(Statut.ACCESSIBLE);
			}
		} else {
			// Sinon elle ne sera pas du tout accessible
			if(unite==null) {
				label = "X";
			} else {
				List<ControleurCoordonnee> voisins = this.voisinsAccessibles(numeroCaseActuelle);
				for(indiceVoisins = 0; indiceVoisins < voisins.size(); indiceVoisins++) {
					this.get(coordonneeActuelle, voisins.get(indiceVoisins)).setStatut(Statut.ACCESSIBLE);
				}
				label = ""+unite.getLabel();
			}
		}
		this.get(coordonneeActuelle).setStatut(statut);
		this.get(coordonneeActuelle).setLabel(label);
		this.get(coordonneeActuelle).setUnitePresente(unite);
	}
	
	@Override
	public String toString() {
		// Permet de tester les affectations
		String retour="";
		for(int i = 0; i < this.taille(); i++) {
			for(int j = 0; j < this.taille(); j++) {
				if(this.get(ControleurGrapheCase.getCoordonnee(i), ControleurGrapheCase.getCoordonnee(j)).getStatut()==Statut.ACCESSIBLE) {
					retour += 1+" ";
				} else {
					retour += 0+" ";
				}
				if(j == (this.taille()-1)) {
					retour += "\n";
				}
			}
		}
		return retour;
	}
	
	public List<ControleurCoordonnee> voisinsAccessibles(int numeroDeCase) {
		// Retourne les voisins accessibles
		return this.voisinsAccessibles(numeroDeCase, -1);
	}
	
	public List<ControleurCoordonnee> voisinsAccessibles(int numeroDeCase, int numeroDeCaseArrivee) {
		// Retourne les voisins accessibles
		// On passe la case d'arrivee en parametre pour qu'elle ne soit pas ignoree dans la liste des voisins
		// Car selon l'hypothese, aucune unite ne peut passer par la case d'arrivee ou de depart d'un autre
		
		int j = 0;
		int numeroDeCaseVoisin;
		List<ControleurCoordonnee> voisins = ControleurGrapheCase.voisins(numeroDeCase);
		List<ControleurCoordonnee> voisinsAccessibles = new ArrayList<ControleurCoordonnee>();
		while(j < voisins.size()) {
			numeroDeCaseVoisin=ControleurGrapheCase.getNumeroDeCase(voisins.get(j).getX(), voisins.get(j).getY());
			// Il faut que la case voisine soit accessible et que ce ne soit pas une case d'arrivee a moins
			// que ce soit la case d'arrivee passee en parametre (sert pour le calcul de trajectoire)
			if(this.get(numeroDeCaseVoisin, numeroDeCaseVoisin).getStatut().equals(Statut.ACCESSIBLE) &&
					(this.get(numeroDeCaseVoisin, numeroDeCaseVoisin).getLabel().charAt(0)!='a' || 
					numeroDeCaseVoisin == numeroDeCaseArrivee)) {
				voisinsAccessibles.add(voisins.get(j));
			}
			j++;
		}
		return voisinsAccessibles;
	}
	
	public ControleurTrajectoire calculPlusCourtChemin(ControleurObjectif objectif) throws HypothesesNonRespecteesException {
		if(objectif != null) {
			// On cree le tableau qui contiendra les poids de chaque case vis a vis
			// d'une case parent, c'est a dire une case a partir de laquelle on 
			// peut y acceder
			ControleurPoidsCase[] poids = new ControleurPoidsCase[this.taille()];
			for(int i = 0; i < this.taille(); i++) {
				poids[i] = new ControleurPoidsCase();
			}
			
			// Declaration des listes
			List<Integer> aVisiter = new ArrayList<Integer>();
			List<Integer> visites = new ArrayList<Integer>();
			List<ControleurCoordonnee> voisins = new ArrayList<ControleurCoordonnee>();
			List<Integer> trajectoireTemporaire = new ArrayList<Integer>();
			List<ControleurCoordonnee> trajectoire = new ArrayList<ControleurCoordonnee>();
			
			// Calcul des numeros de case de depart et d'arrivee
			int numeroDeCaseVoisin;
			int numeroDeCaseActuel =-1;
			int numeroDeCaseDepart = ControleurGrapheCase.getNumeroDeCase(objectif.getXDepart(), objectif.getYDepart());
			int numeroDeCaseArrivee = ControleurGrapheCase.getNumeroDeCase(objectif.getXArrivee(), objectif.getYArrivee());
			// On affecte 0 au poids de la case de depart et on la marque comme visitee
			// et chaque PoidsCase est initialisé à + l'infini
			poids[numeroDeCaseDepart].setPoids(0);
			visites.add(numeroDeCaseDepart);
			numeroDeCaseActuel = numeroDeCaseDepart;
			
			// On marque tout ses voisins dans la liste aVisiter
			voisins = this.voisinsAccessibles(numeroDeCaseDepart, numeroDeCaseArrivee);
			ControleurGrapheCase.fusionnerListes(aVisiter, voisins, visites);
			
			// On initialise toutes les cases aVisiter a un poids 1
			for(int j = 0; j < aVisiter.size(); j++) {
				poids[aVisiter.get(j)].setPoids(1);
				poids[aVisiter.get(j)].setNumeroDeCaseParent(numeroDeCaseDepart);
			}
			
			int poidsMin;
			while(numeroDeCaseActuel != numeroDeCaseArrivee) {
				// On recupere le numero d'une case a visiter
				poidsMin=ControleurGrapheCase.caseDePlusPetitPoids(aVisiter, poids);
				numeroDeCaseActuel = aVisiter.get(poidsMin);
				// On l'enleve de la liste
				aVisiter.remove(poidsMin);
				// On recupere ses voisins
				voisins = this.voisinsAccessibles(numeroDeCaseActuel, numeroDeCaseArrivee);
				ControleurGrapheCase.fusionnerListes(aVisiter, voisins, visites);
				// On la marque comme visite
				visites.add(numeroDeCaseActuel);
				
				for(int i = 0; i < voisins.size(); i++) {
					numeroDeCaseVoisin = ControleurGrapheCase.getNumeroDeCase(voisins.get(i).getX(), voisins.get(i).getY());
					if((poids[numeroDeCaseActuel].getPoids()+1) < poids[numeroDeCaseVoisin].getPoids()) {
						// Si le poids du voisin est inferieur au poids de la case actuelle + 1
						// alors on affecte au voisin en question, comme voisin direct la case actuelle
						poids[numeroDeCaseVoisin].setPoids(poids[numeroDeCaseActuel].getPoids()+1);
						poids[numeroDeCaseVoisin].setNumeroDeCaseParent(numeroDeCaseActuel);
					}
				}
			}
			
			// Une fois que les poids sont initialises, on peut maintenant creer une liste contenant la trajectoire a l'envers
			numeroDeCaseActuel = numeroDeCaseArrivee;
			if(poids[numeroDeCaseActuel].getNumeroDeCaseParent()!=-1) {
				while(numeroDeCaseActuel!=-1) {
					trajectoireTemporaire.add(numeroDeCaseActuel);
					numeroDeCaseActuel = poids[numeroDeCaseActuel].getNumeroDeCaseParent();
				}
			
				// Il faut maintenant remettre les cases dans le bon ordre et transformer
				// les numeros de cases en coordonnees
			
				for(int i = trajectoireTemporaire.size()-1; i >= 0; i--) {
					ControleurCoordonnee coordonneeCase = ControleurGrapheCase.getCoordonnee(trajectoireTemporaire.get(i));
					trajectoire.add(coordonneeCase);
				}
			}
			
			return new ControleurTrajectoire(trajectoire);
		} else {
			return new ControleurTrajectoire();
		}
	}
	
	// Methode(s) statique(s)
	private static int getNumeroDeCase(int lig, int col) {
		// Retourne le numero de case correspondant aux coordonnees passees en parametre
		// Ce numero de case sert pour la matrice d'adjacence
		
		return (Ressources.NOMBRE_DE_COLONNES)*lig + col;
	}
	
	private static ControleurCoordonnee getCoordonnee(int numeroDeCase) {
		// A partir d'un numero de case passe en argument,
		// la fonction retourne les coordonnees sur les plateau correspondantes
		return new ControleurCoordonnee(numeroDeCase/Ressources.NOMBRE_DE_COLONNES, numeroDeCase%Ressources.NOMBRE_DE_COLONNES);
	}
	
	public static List<ControleurCoordonnee> voisins(int lig, int col) {
		List<ControleurCoordonnee> voisins = new ArrayList<ControleurCoordonnee>();
		
		// Voisin du haut
		if(lig != 0) {
			voisins.add(new ControleurCoordonnee(lig-1, col));
		}
		
		// Voisin du bas
		if(lig != (Ressources.NOMBRE_DE_LIGNES-1)) {
			voisins.add(new ControleurCoordonnee(lig+1, col));
		}
		
		// Voisin de gauche
		if(col != 0) {
			voisins.add(new ControleurCoordonnee(lig, col-1));
		}
		
		// Voisin de droite
		if(col != (Ressources.NOMBRE_DE_COLONNES-1)) {
			voisins.add(new ControleurCoordonnee(lig, col+1));
		}
		
		return voisins;
	}
	
	private static List<ControleurCoordonnee> voisins(int numeroDeCase) {
		// Retourne tous les voisins (accessibles ou non) de la case numeroDeCase
		ControleurCoordonnee coordonnee = ControleurGrapheCase.getCoordonnee(numeroDeCase);
		int lig = coordonnee.getX();
		int col = coordonnee.getY();
		List<ControleurCoordonnee> voisins = new ArrayList<ControleurCoordonnee>();
		
		// Voisin du haut
		if(lig != 0) {
			voisins.add(new ControleurCoordonnee(lig-1, col));
		}
		
		// Voisin du bas
		if(lig != (Ressources.NOMBRE_DE_LIGNES-1)) {
			voisins.add(new ControleurCoordonnee(lig+1, col));
		}
		
		// Voisin de gauche
		if(col != 0) {
			voisins.add(new ControleurCoordonnee(lig, col-1));
		}
		
		// Voisin de droite
		if(col != (Ressources.NOMBRE_DE_COLONNES-1)) {
			voisins.add(new ControleurCoordonnee(lig, col+1));
		}
		return voisins;
	}
	
	private static List<Integer> fusionnerListes(List<Integer> aVisiter, List<ControleurCoordonnee> voisins, List<Integer> visites) {
		// On met tous les elements de voisins dans aVisiter
		// Il faut neanmoins bien faire attention a convertir les coordonnees en numeros de case
		ControleurCoordonnee coordonnee;
		int numeroDeCase;
		for(int i = 0; i < voisins.size(); i++) {
			coordonnee = voisins.get(i);
			numeroDeCase = ControleurGrapheCase.getNumeroDeCase(coordonnee.getX(), coordonnee.getY());
			if(!aVisiter.contains(numeroDeCase) && !visites.contains(numeroDeCase)) {
				aVisiter.add(numeroDeCase);
			}
		}
		return aVisiter;
	}
	
	private static int caseDePlusPetitPoids(List<Integer> aVisiter, ControleurPoidsCase[] poids) throws HypothesesNonRespecteesException {
		// Retourne la case de plus petit poids dans le tableau 'poids'
		int plusPetit = Integer.MAX_VALUE;
		int casePlusPetitPoids = -1;
		for(int i = 0; i < aVisiter.size(); i++) {
			if(poids[aVisiter.get(i)].getPoids() < plusPetit) {
				plusPetit = poids[aVisiter.get(i)].getPoids();
				casePlusPetitPoids = i;
			}
		}
		if(casePlusPetitPoids == -1) {
			throw new HypothesesNonRespecteesException();
		}
		return casePlusPetitPoids;
	}
}
